package com.ares.basic.http;

import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Sets;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.Asserts;
import org.apache.http.util.EntityUtils;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Getter
@Setter
@Slf4j
public class PoolingHttpExecutor {

    /**
     * 计算消费的毫秒数时的除数
     */
    private static final double CONSUME_MILLIS_DIVISOR = 1000000d;

    private static final String URL_PARAMETER_SEPARATOR = "?";

    private static final String URL_PARAMETER_UNION = "&";

    private static final String BEFORE_REQUEST_LOG_TEMPLATE = "Request: [Url]{} [Method]{} [Headers]{} [Params]{}";

    private static final String AFTER_REQUEST_LOG_TEMPLATE = "Response: [Url]{} [Result]{}";

    /**
     * 默认连接超时时间
     */
    private static final int DEFAULT_CONNECTION_TIMEOUT = 3000;

    /**
     * 默认连接池最大连接数
     */
    private static final int DEFAULT_MAX_TOTAL = 300;

    /**
     * 默认单个路由最大连接数
     */
    private static final int DEFAULT_MAX_PER_ROUTE = DEFAULT_MAX_TOTAL;

    /**
     * 默认等待数据超时时间
     */
    private static final int DEFAULT_SO_TIMEOUT = 5000;

    /**
     * 默认等待从连接池获取连接超时时间
     */
    private static final int DEFAULT_WAIT_CONNECTION_TIMEOUT = 2000;

    /**
     * 默认连接不活跃多久后校验，单位毫秒
     */
    private static final int DEFAULT_INACTIVE_VALIDATE_INTERVAL = 5000;

    /**
     * 默认连接存活时间，单位秒
     */
    private static final int DEFAULT_CONNECTION_LIVE_SECONDS = 50;

    /**
     * 默认user agent
     */
    private static final String DEFAULT_USER_AGENT = "vcloud web http client";

    /**
     * 默认Http Header
     */
    private static final Set<Header> DEFAULT_HEADERS =
            Sets.<Header>newHashSet(new BasicHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType()));

    /**
     * 默认不开启重试
     */
    private static final HttpRequestRetryHandler DEFAULT_RETRY_HANDLER = new DefaultHttpRequestRetryHandler(0, false);

    /**
     * 默认CookieStore
     */
    private static final CookieStore DEFAULT_COOKIE_STORE = new BasicCookieStore();

    private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
    private int maxPerRoute = DEFAULT_MAX_PER_ROUTE;
    private int maxTotal = DEFAULT_MAX_TOTAL;
    private int socketTimeout = DEFAULT_SO_TIMEOUT;
    private Charset requestCharset = Consts.UTF_8;
    private Charset responseCharset = Consts.UTF_8;
    private int inactiveValidateInterval = DEFAULT_INACTIVE_VALIDATE_INTERVAL;
    private int waitConnectionTimeout = DEFAULT_WAIT_CONNECTION_TIMEOUT;
    private int connectionLiveSeconds = DEFAULT_CONNECTION_LIVE_SECONDS;

    /**
     * 是否使用系统SSL
     */
    private boolean useSystemSSL = true;
    private String userAgent = DEFAULT_USER_AGENT;
    private Set<Header> headers = DEFAULT_HEADERS;
    private CookieStore cookieStore = DEFAULT_COOKIE_STORE;

    private HttpRequestRetryHandler retryHandler = DEFAULT_RETRY_HANDLER;

    private PoolingHttpClientConnectionManager manager = null;

    private CloseableHttpClient httpClient = null;

    /**
     * 初始化 poolManager 以及 httpClient
     * <p>
     * Https协议默认用操作系统的SSL库<br/>
     * HttpConnectionFactory默认用{@link org.apache.http.impl.conn.ManagedHttpClientConnectionFactory#INSTANCE}<br/>
     * <br/>
     * </p>
     */
    @PostConstruct
    public void afterPropertiesSet() {
        // 注册访问协议相关的 socket factory, https协议如果不用操作系统的，则用TLS
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .register("https", this.useSystemSSL ? SSLConnectionSocketFactory.getSystemSocketFactory()
                        : SSLConnectionSocketFactory.getSocketFactory())
                .build();
        this.manager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        // 设置默认socket config
        SocketConfig defaultSocketConfig = SocketConfig.DEFAULT;
        this.manager.setDefaultSocketConfig(defaultSocketConfig);
        // 设置连接池最大连接数
        this.manager.setMaxTotal(this.maxTotal);
        // 设置每个路由(IP:PORT)上的默认最大连接
        this.manager.setDefaultMaxPerRoute(this.maxPerRoute);
        // 设置从连接池获取连接时，当连接不活跃多长时间后进行一次验证，默认值为2s，此处修改为5s
        this.manager.setValidateAfterInactivity(this.inactiveValidateInterval);
        // 默认请求配置
        RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(this.connectionTimeout)
                .setSocketTimeout(this.socketTimeout).setConnectionRequestTimeout(this.waitConnectionTimeout).build();
        // 创建httpClient
        this.httpClient = HttpClients.custom().setConnectionManager(this.manager)
                // 定期回收过期连接
                .evictExpiredConnections()
                // 设置默认连接配置
                .setDefaultRequestConfig(defaultRequestConfig)
                // 设置连接池不是共享的，一个实例一个pool，无需共享
                .setConnectionManagerShared(false)
                // 设置连接存活时间，如果不设置则根据长连接设置决定
                .setConnectionTimeToLive(this.connectionLiveSeconds, TimeUnit.SECONDS)
                // 连接重用策略，即是否能keep alive
                .setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE)
                // 长连接配置，即长连接允许keep alive多久
                .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
                // 重试机制
                .setRetryHandler(this.retryHandler == null ? DEFAULT_RETRY_HANDLER : this.retryHandler)
                // 设置user agent
                .setUserAgent(this.userAgent != null ? userAgent : DEFAULT_USER_AGENT)
                // 设置默认Headers
                .setDefaultHeaders(headers == null || headers.isEmpty() ? DEFAULT_HEADERS : headers)
                // 设置CookieStore
                .setDefaultCookieStore(cookieStore == null ? DEFAULT_COOKIE_STORE : cookieStore).build();
    }

    /**
     * JVM停止或重启时，关闭连接池释放连接
     */
    @PreDestroy
    public void destroy() {
        log.info("http client pool will destroy...");
        if (this.manager != null) {
            this.manager.shutdown();
        }
        log.info("http client pool destroyed");
    }

    /**
     * Post方式发送application/x-www-form-urlencoded请求
     *
     * @param url     url
     * @param map     请求参数
     * @param headers 请求headers
     */
    public HttpResult postFormData(String url, Map<String, Object> map,
                                   Header[] headers) {
        List<NameValuePair> params = new ArrayList<>();
        if (map != null) {
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                params.add(new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue())));
            }
        }
        return postFormData(url, params, headers, null);
    }

    /**
     * Post方式发送application/json请求
     *
     * @param url     url
     * @param map     请求参数
     * @param headers 请求headers
     */
    public HttpResult postJsonData(String url, Map<String, Object> map,
                                   Header[] headers) {
        try {
            HttpPost httpPost = new HttpPost(url);
            setHeaders(httpPost, headers);
            httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON
                    .withCharset(getCharsetDefaultUTF8(this.requestCharset)).toString());
            if (map != null) {
                String body = JSONObject.toJSONString(map);
                StringEntity entity = new StringEntity(body, Consts.UTF_8);
                httpPost.setEntity(entity);
            }
            return wrapExecute(httpPost);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return null;
    }

    /**
     * Post方式发送application/json请求
     *
     * @param url     url
     * @param body    请求参数
     * @param headers 请求headers
     */
    public HttpResult postJsonData(String url, String body,
                                   Header[] headers) {
        try {
            HttpPost httpPost = new HttpPost(url);
            if (StringUtils.isNotEmpty(body)) {
                StringEntity entity = new StringEntity(body, Consts.UTF_8);
                httpPost.setEntity(entity);
            }
            setHeaders(httpPost, headers);
            httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON
                    .withCharset(getCharsetDefaultUTF8(this.requestCharset)).toString());
            return wrapExecute(httpPost);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return null;
    }


    /**
     * Post方式发送application/x-www-form-urlencoded请求
     *
     * @param url     url
     * @param params  请求参数
     * @param headers 请求headers
     */
    public HttpResult postFormData(String url, List<NameValuePair> params,
                                   Header[] headers) {
        return postFormData(url, params, headers, null);
    }

    /**
     * Post方式发送application/x-www-form-urlencoded请求
     *
     * @param url           url
     * @param params        请求参数
     * @param headers       请求headers
     * @param requestConfig 为null时表示依赖pool manager的默认配置
     */
    public HttpResult postFormData(String url, List<NameValuePair> params,
                                   Header[] headers, RequestConfig requestConfig) {
        HttpPost httpPost = new HttpPost(url);
        setHeaders(httpPost, headers);
        if (requestConfig != null) {
            httpPost.setConfig(requestConfig);
        }
        if (params == null || params.isEmpty()) {
            httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED
                    .withCharset(getCharsetDefaultUTF8(this.requestCharset)).toString());
        } else {
            UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, getCharsetDefaultUTF8(this.requestCharset));
            httpPost.setEntity(entity);
        }
        return wrapExecute(httpPost);
    }


    /**
     * 执行请求并封装请求结果为HttpResult
     *
     * @param request request
     */
    private HttpResult wrapExecute(HttpRequestBase request) {
        Asserts.notNull(request, "http request");
        HttpResponse response = null;
        HttpResult httpResult;
        long startNanoTime = System.nanoTime();
        long endNanoTime;
        double consumeMillis;
        String result = null;
        try {
            response = this.httpClient.execute(request);
            endNanoTime = System.nanoTime();
            consumeMillis = calculateConsumeMillis(startNanoTime, endNanoTime);
            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                // http error
                // http error
                httpResult = new HttpResult(response.getStatusLine().getStatusCode(),
                        response.getStatusLine().getReasonPhrase(), consumeMillis, response.getAllHeaders());
            } else {
                // http ok
                String responseData = EntityUtils.toString(response.getEntity(),
                        responseCharset == null ? Consts.UTF_8 : responseCharset);
                httpResult = new HttpResult(responseData, consumeMillis, response.getAllHeaders());
            }
        } catch (IOException e) {
            endNanoTime = System.nanoTime();
            consumeMillis = calculateConsumeMillis(startNanoTime, endNanoTime);
            log.error("execute request {} error, consume mills {}", request.getURI(), consumeMillis, e);
            httpResult = new HttpResult(HttpResult.HTTP_THROW_EXCEPTION, e.getMessage(), consumeMillis, null);
        } finally {
            HttpClientUtils.closeQuietly(response);
        }
        return httpResult;
    }

    /**
     * Get方式发送application/x-www-form-urlencoded请求
     *
     * @param url    url
     * @param params 请求参数
     * @return {@link HttpResult}
     */
    public HttpResult getFormData(String url, List<NameValuePair> params) {
        HttpGet httpGet = buildHttpGet(url, params);
        HttpResult httpResult = wrapExecute(httpGet);
        return httpResult;
    }

    /**
     * 构建HttpGet请求
     *
     * @param url    url
     * @param params 请求参数
     * @return {@link HttpGet}
     */
    private HttpGet buildHttpGet(String url, List<NameValuePair> params) {
        if (params == null || params.isEmpty()) {
            return new HttpGet(url);
        }
        String param = URLEncodedUtils.format(params, getCharsetDefaultUTF8(this.requestCharset));
        if (!url.contains(URL_PARAMETER_SEPARATOR)) {
            return new HttpGet(url + URL_PARAMETER_SEPARATOR + param);
        } else if (!url.endsWith(URL_PARAMETER_UNION) && !url.endsWith(URL_PARAMETER_SEPARATOR)) {
            return new HttpGet(url + URL_PARAMETER_UNION + param);
        } else {
            return new HttpGet(url + param);
        }
    }

    /**
     * 计算消费的毫秒数
     *
     * @param startNanoTime 起始纳秒
     * @param endNanoTime   结束纳秒
     * @return 消耗的毫秒数
     */
    private double calculateConsumeMillis(long startNanoTime, long endNanoTime) {
        if (startNanoTime == endNanoTime) {
            return 0;
        }
        return (endNanoTime - startNanoTime) / CONSUME_MILLIS_DIVISOR;
    }

    private Charset getCharsetDefaultUTF8(Charset charset) {
        return charset == null ? Consts.UTF_8 : charset;
    }

    /**
     * 设置http header
     */
    private void setHeaders(HttpRequestBase request, Header[] headers) {
        if (headers != null && headers.length > 0) {
            for (Header header : headers) {
                request.setHeader(header);
            }
        }
    }
}
